import Command from "./command/command"
import Logger from "./utils/Logger"
import OpenDeferred from "./utils/OpenDeferred"
import TypeLookup from "./utils/TypeLookup"
const c = new Logger("command-binder")
interface hookItem {
  preCommandCallback: (v: any) => Promise<any>
  postCommandCallback: (v: any) => Promise<any>
  resolving: boolean
}
export default class CommandBinder {
  typeLookup: TypeLookup
  bindings: Record<string, Function>
  queuedCommands: Record<any, any>
  hookedCommands: Record<any, hookItem>
  constructor() {
    this.typeLookup = new TypeLookup()
    this.bindings = {}
    this.queuedCommands = {}
    this.hookedCommands = {}
    this.issueCommand = this.issueCommand.bind(this)
    this.issueCommandWhenBound = this.issueCommandWhenBound.bind(this)
  }
  resetHooks() {
    this.hookedCommands = {}
  }
  addBinding<B extends typeof Command = typeof Command>(e: B, t: (e?: B["prototype"]["payload"]) => any) {
    const i = this.typeLookup.getKeyByType(e, !0)
    if (this.bindings[i]) throw Error(`Command ${i} already bound to a callback`)
    this.bindings[i] = t
    this.callQueuedCommands(i)
    return {
      renew: () => {
        this.bindings[i] !== t && this.addBinding(e, t)
      },
      cancel: () => this.removeBinding(e, t),
      active: this.bindings[i] === t
    }
  }
  removeBinding<A extends typeof Command>(e: A, t: any) {
    const i = this.typeLookup.getKeyByType(e)
    if (!i) return void c.debug("Trying to unbind unbound command")
    this.bindings[i] === t ? delete this.bindings[i] : c.debug("Trying to unbind unbound callback")
  }
  issueCommand<A extends Command>(e: A, t = !0): Promise<any> {
    const key = this.typeLookup.getKeyByInstance(e)
    const func = this.bindings[key]
    return func ? this.issueCommandHelper(e, key, func, t) : Promise.reject("Command not bound: " + e.id)
  }
  issueCommandHelper<A extends Command>(e: A, key: string, func: Function, _n = !0) {
    return this.hookedCommands[key] && !this.hookedCommands[key].resolving ? this.issueHookCommand(e, key) : func(e.payload)
  }
  issueCommandWhenBound<A extends Command>(e: A) {
    const i = new OpenDeferred()
    this.issueCommand(e)
      .then(e => i.resolve(e))
      .catch(() => {
        const n = this.typeLookup.getKeyByInstance(e, !0)
        this.queuedCommands[n] || (this.queuedCommands[n] = []), this.queuedCommands[n].push({ command: e, deferred: i })
      })
    return i.nativePromise()
  }
  callQueuedCommands(e: string) {
    const t = []
    this.queuedCommands[e] &&
      (this.queuedCommands[e].forEach(e => {
        t.push(this.issueCommand(e.command).then(t => e.deferred.resolve(t)))
      }),
      Promise.all(t).then(() => {
        delete this.queuedCommands[e]
      }))
  }
  hookCommand<A extends typeof Command>(e: A, isPre: boolean, i: (v: any) => Promise<any>) {
    const n = this.typeLookup.getKeyByType(e, !0)
    if (this.hookedCommands[n]) {
      if (isPre && null !== this.hookedCommands[n].preCommandCallback) throw Error(`Command ${e.name} has already been pre hooked`)
      if (!isPre && null !== this.hookedCommands[n].postCommandCallback) throw Error(`Command ${e.name} has already been post hooked`)
    } else
      this.hookedCommands[n] = {
        preCommandCallback: null,
        postCommandCallback: null,
        resolving: !1
      }
    isPre ? (this.hookedCommands[n].preCommandCallback = i) : (this.hookedCommands[n].postCommandCallback = i)
  }
  async issueHookCommand<A extends Command>(e: A, t: string) {
    const i = this.hookedCommands[t]
    const n = this.bindings[t]
    const s = e.payload
    const r = i.preCommandCallback
    const o = i.postCommandCallback
    try {
      r && (await r(s))
      i.resolving = !0
      const a = await this.issueCommandHelper(e, t, n, !0)
      i.resolving = !1
      o && (await o(s))
      return a
    } catch (e) {
      throw Error(`Could not completely issueHookCommand - ${e}`)
    }
  }
}
